/*
 * The contents of this file are subject to the GNU Lesser General Public
 * License Version 2.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.gnu.org/copyleft/lesser.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Developer:
 * Todd Ditchendorf, todd@ditchnet.org
 *
 */

/**
 *	@author Todd Ditchendorf
 *	@since 2005-03-11
 *
 */

package it.blueocean.acanto.taglibrary.jsp.util;

import it.blueocean.acanto.taglibrary.xml.Xml;

import java.io.StringWriter;
import java.io.Writer;

/**
 * A JSP-specific XML writing utility class meant to be used as an output buffer
 * in a JSP tag handler subclass of {@link javax.servlet.jsp.tagext.SimpleTag}.
 * This class should not be viewed as a general purpose XML writing utility
 * class as several of it's methods make assumptions about the formatting of the
 * output that make it's use in a JSP tag handler class more natural. Note that
 * this class is not thread safe.
 * 
 * @author Todd Ditchendorf
 * @version 0.8
 */
public final class JspResponseWriter
{

	private static final String LT = "<";

	private static final String GT = ">";

	private static final String COLON = ":";

	private static final String SPACE = " ";

	private static final String EQUALS = "=";

	private static final String TAB_STOP = "\t";

	private static final String LINE_BREAK = "\n";

	private static final String SINGLE_QUOTE = "'";

	private static final String DOUBLE_QUOTE = "\"";

	private static final String OPEN_COMMENT = "<!-- ";

	private static final String CLOSE_COMMENT = " -->";

	private static final String FORWARD_SLASH = "/";

	private StringWriter out = new StringWriter();

	private boolean prettyPrinting = true;

	private int tagDepth;

	private volatile boolean elementTagIsOpen;

	private volatile boolean elementIsEmpty;

	/**
	 * Defualt constructor -- Sets pretty printing to 'on'.
	 * 
	 */
	public JspResponseWriter()
	{
		this(true);
	}

	/**
	 * Allows instanciating this writer with pretty printing turned off.
	 */
	public JspResponseWriter(final boolean prettyPrinting)
	{
		setPrettyPrinting(prettyPrinting);
	}

	/**
	 *	
	 */
	public StringBuffer getBuffer()
	{
		return out.getBuffer();
	}

	public Writer getWriter()
	{
		return out;
	}

	/**
	 * Set this writer to include line breaks and tabstops in the output for
	 * easier human-readable markup.
	 */
	public void setPrettyPrinting(final boolean prettyPrinting)
	{
		this.prettyPrinting = prettyPrinting;
	}

	/**
	 * Check for pretty printing setting.
	 */
	public boolean isPrettyPrinting()
	{
		return prettyPrinting;
	}

	private void prettyPrint()
	{
		if(!isPrettyPrinting())
		{
			return;
		}
		lineBreak();
		for (int i = 0; i < tagDepth; i++)
		{
			tabStop();
		}
		tagDepth++;
	}

	public void tabStop()
	{
		closeOpenTagIfNecessary();
		out.write(TAB_STOP);
		elementIsEmpty = false;
	}

	/**
	 * Start an XML element with a qualified name, up to and including the
	 * element name. Once this method has been called, clients can call the
	 * <code>writeAttribute()</code> or methods to add attributes and
	 * corresponding values. The starting element will be closed (that is, the
	 * trailing '>' character added) on any subsequent call to
	 * <code>startElement()</code>, <code>comment()</code>, lineBreak(),
	 * <code>text()</code>, <code>endElement()</code></p>
	 */
	public void startElement(Xml.Tag qName)
	{
		_startElement(qName.toString());
	}

	/**
	 * Start an XML element with a namespace prefix and a localName.
	 */
	public void startElement(String prefix, Xml.Tag localName)
	{
		_startElement(prefix + COLON + localName);
	}

	private void _startElement(final String name)
	{
		closeOpenTagIfNecessary();
		prettyPrint();
		out.write(LT);
		out.write(name);
		elementTagIsOpen = true;
		elementIsEmpty = true;
	}

	/**
	 * End an XML element with a qualified name.
	 */
	public void endElement(Xml.Tag qName)
	{
		_endElement(qName.toString());
	}

	/**
	 * End an XML element with a namespace prefix and a localName.
	 */
	public void endElement(String prefix, Xml.Tag localName)
	{
		_endElement(prefix + COLON + localName);
	}

	private void _endElement(final String name)
	{
		if(elementIsEmpty)
		{
			out.write(SPACE);
			out.write(FORWARD_SLASH);
			out.write(GT);
			elementIsEmpty = false;
		}
		else
		{
			if(isPrettyPrinting())
			{
				lineBreak();
			}
			out.write(LT);
			out.write(FORWARD_SLASH);
			out.write(name);
			out.write(GT);
		}
		elementTagIsOpen = false;
		tagDepth--;
	}

	/**
	 * This method exists for the convenience of the JSP author in manually
	 * controlling the pretty printing of the buffered output.
	 */
	public void lineBreak()
	{
		closeOpenTagIfNecessary();
		out.write(LINE_BREAK);
		elementIsEmpty = false;
	}

	/**
	 * Write an attribute with qualified name and corresponding value. This
	 * method may only be called after a call to <code>startElement()</code>,
	 * and before the element has been closed.
	 * 
	 * @exception <code>NullPointerException</code>
	 * @exception <code>IllegalStateException</code> if no element tag is
	 *            currently open
	 */
	public void attribute(Xml.Attr qName, String value)
	{
		_attribute(qName.toString(), value);
	}

	/**
	 * Write an attribute with this namespace prefix, and local name
	 * corresponding values. This method may only be called after a call to
	 * <code>startElement()</code>, and before the element has been closed.
	 * 
	 * @exception <code>NullPointerException</code>
	 * @exception <code>IllegalStateException</code> if no element tag is
	 *            currently open
	 */
	public void attribute(String prefix, Xml.Attr localName, String value)
	{
		_attribute(prefix + localName.toString(), value);
	}

	private void _attribute(String qName, String value)
	{
		if(!elementTagIsOpen)
		{
			throw new IllegalStateException("Attempting to write attribute " + " with no open element tag");
		}
		if(null == qName)
		{
			throw new NullPointerException("Attempting to write attribute " + " with no name");
		}
		out.write(SPACE);
		out.write(qName);
		out.write(EQUALS);
		out.write(DOUBLE_QUOTE);
		out.write(value);
		out.write(DOUBLE_QUOTE);
	}

	/**
	 * <p>
	 * Write an object, after converting it to a String (if necessary), and
	 * after performing any escaping appropriate for the markup language being
	 * rendered. If there is an open element that has been created by a call to
	 * <code>startElement()</code>, that element will be closed first.
	 * </p>
	 * 
	 * @param text
	 *            Text to be written
	 * 
	 * @exception NullPointerException
	 *                if <code>text</code> is <code>null</code>
	 */
	public void text(Object text)
	{
		if(null == text)
		{
			throw new NullPointerException("Attempting to write null text");
		}
		closeOpenTagIfNecessary();
		out.write(text.toString());
		elementIsEmpty = false;
	}

	/**
	 * <p>
	 * Write a comment containing the specified text, after converting that text
	 * to a String (if necessary), and after performing any escaping appropriate
	 * for the markup language being rendered. If there is an open element that
	 * has been created by a call to <code>startElement()</code>, that element
	 * will be closed first.
	 * </p>
	 * 
	 * @param comment
	 *            Text content of the comment
	 * 
	 * @exception NullPointerException
	 *                if <code>comment</code> is <code>null</code>
	 */
	public void comment(Object comment)
	{
		if(null == comment)
		{
			throw new NullPointerException("Attempting to write null comment");
		}
		closeOpenTagIfNecessary();
		out.write(OPEN_COMMENT);
		out.write(comment.toString());
		out.write(CLOSE_COMMENT);
	}

	private void closeOpenTagIfNecessary()
	{
		if(elementTagIsOpen)
		{
			out.write(GT);
			elementTagIsOpen = false;
		}
	}

}
